package Phase1;

import java.io.IOException;
import static org.junit.Assert.*;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.ServerSocket;
import java.net.Socket; 
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedList;
//import java.util.Iterator;
import java.util.Set;

//import Phase2.Node;
/**
 * here is the Server side class.
 * It is a Thread that always running for hearing request from other node.
 * @author djb0646
 * @version 0.9
 */
public class Server extends Thread
{
	
	private ServerSocket hearingPort;  
	private volatile boolean run = true;
	private ObjectDB database;
	private final int defaultPort = 3000;
	private final int SLEEP_100 = 100;
	private final int TIMEOUT = 15000;
	private int dataPort = defaultPort;
	private ArrayList<Worker> workSpace;
	private boolean debug;
	/**
	 * constructor of Server()
	 * create a socket(hearingPort) that listens for requests from clients at port 3000 by default.
	 * 
	 */
	public Server()
	{   
		
		try 
		{  
			hearingPort = new ServerSocket(defaultPort);
			
		} catch (IOException e) 
		{
			e.printStackTrace();
		}
		this.init();
	}
	/**
	 * constructor with parameter
	 * @param port instead of port 3000, use specified port number that listens for requests.
	 */
	public Server(int port) throws IOException
	{   
		if(port < 1024 || port > 65535)
		{
			throw new IOException("Invalid port number. Port number must be 1024-65535 inclusive.");
		}
		hearingPort = new ServerSocket(port);
				
		this.init();
	}
	/**
	 * initiate threads in workspace and database. start server as thread.
	 */
	private void init()
	{
		setUpWorkSpace(); 
		database = ObjectDB.getDatabase();
		this.start();		
	}
	
	/**
	 * set up list of Threads that can processing request at same time.
	 * 
	 */ 
	private void setUpWorkSpace()
	{
		workSpace = new ArrayList<Worker>();
		workSpace.add(new Worker());
		workSpace.add(new Worker());
		workSpace.add(new Worker());
		workSpace.add(new Worker());
		workSpace.add(new Worker());
	}
	
	/**
	 * get one Thread that are ready to work with command.
	 * @pre list of thread has been initiated and start.
	 * @return a thread of worker
	 */
	public Worker getFreeWorkSpace()
	{
		Worker result = null;
		for(int index = 0; index < workSpace.size(); index++)
		{
			if(workSpace.get(index).ready())
			{
				result =  workSpace.get(index);
			}

		}
		if(result ==  null)
		{
			result =  new Worker(); 
			workSpace.add(result);
		}
		return result;
	}
	/**
	 * continues listens for requests from client. Create new data channel for each received
	 * client request, and process next received command request.
	 * @see java.lang.Thread#run()
	 */
	public void run()
	{  
		print("|Server|->run()");
		 
		while (run) 
		{ 
			try
			{				
				listenData();
				Thread.sleep(SLEEP_100);
			} 
			catch (IOException e) 
			{
				System.out.println("Exception " + e.getClass().getName() + " " + e.getMessage());  
			} 
			catch (ClassNotFoundException e) 
			{
				System.out.println("Exception " + e.getClass().getName() + " " + e.getMessage());
			}
			catch (InterruptedException e) 
			{
				System.out.println("Exception " + e.getClass().getName() + " " + e.getMessage());
			}
		}
		
		
		try
		{ 
			if(hearingPort != null && !hearingPort.isClosed())
			{
				hearingPort.close();
			}
		}
		catch (IOException e)
		{
			;
		}
		 
		 
	}
	
	/**
	 * 
	 * Listen a communication request from common socket (default as 3000). 
	 * Then create a new special port for this Client. 		 
	 * Client will send its remote request to server and waiting for result through the new connection.		 
	 *  
	 * @pre a hearing port is ready for listening
	 * @post a specific socket for data transfer is setted up. and goes to individuallink & processingRequest method.
	 * 
	 *
	 */
	public void listenData()  throws IOException, ClassNotFoundException
	{	 
		Socket source = hearingPort.accept();
		//source.setSoTimeout(TIMEOUT);
		
		//print("|Server|new requst from" + source.getRemoteSocketAddress());
		ObjectOutputStream output = new ObjectOutputStream(source.getOutputStream()); 
		ObjectInputStream input = new ObjectInputStream(source.getInputStream());
		
		dataPort = defaultPort + (dataPort % defaultPort); 
		dataPort++;
		ServerSocket temp;
		try
		{
 	
			temp = new ServerSocket(dataPort);
			//temp.setSoTimeout(TIMEOUT);
			
			output.writeInt(dataPort);
		 	output.flush();
			output.close();
			input.close(); 
			source.close();
			 
			source = temp.accept();
			//source.setSoTimeout(TIMEOUT);
			temp.close();
			
			getFreeWorkSpace().setWorkingPort(source);	
		}
		catch (SocketTimeoutException e)
		{
			System.out.println("SocketTimeOutException while setting up link to Client.");
		}
		catch (IOException e)
		{
			dataPort++;
			System.out.println("IOException while setting up link to Client.");
		}		
	} 
	 
	/**
	 * halt the server immediately
	 */
	public void done()
	{
		run = false;
		for(int i = 0; i < workSpace.size(); i++)
		{
			workSpace.get(i).done();
		}
		try
		{
		//	Client.sendData(InetAddress.getLocalHost().getHostAddress() 
		//					, defaultOutPort, "Kill the server!!!");
			if(hearingPort != null && !hearingPort.isClosed())
			{
				//print("|Server|Stopping server\n");
				hearingPort.close();
				
			}
		} 
		catch (UnknownHostException e) 
		{
			;
			 
		}
		catch (IOException e)
		{
			;
			 
		}  
		
	}
	/**
	 * print debug messages
	 * @param obj 
	 * parameter to be printed
	 */
	private void print(Object obj)
	{
		if(debug)
		{
			System.out.println(obj);
		}
		
	}
	/**
	 * setting printing mode
	 * @param debug
	 * 
	 */
	public void setDebug(boolean debug)
	{
		this.debug = debug;
	}
	/**
	 * left for testing perpose.
	 * @param args
	 */
	public static void main(String[] args)
	{
		Server s = new Server();
	}
	
	/**
	 * process command for one specific client.
	 * @author djb0646
	 *
	 */
	public class Worker extends Thread 
	{
 
		private volatile boolean run = true;
		private ObjectDB database;
		private final int SLEEP_100 = 100;
		private boolean working;
		private Socket workingPort;
		/**
		 * constructor
		 */
		public Worker()
		{ 
			working = false; 
			workingPort = null; 
			database = ObjectDB.getDatabase();
			this.start();	 
		}
		 
		public Worker(Socket temp)
		{ 
			working = false; 
			workingPort = temp; 
			database = ObjectDB.getDatabase();
			this.start();	 
		}
		/**
		 * if a client socket is ready to process, the loop will handle it.
		 */
		public void run()
		{
			while (run) 
			{  
				try 
				{
					if(workingPort != null && ready())
					{
						
						individualLink(workingPort);
						if(!workingPort.isClosed())
						{
							workingPort.close();
						}
						this.stopWorking();
					}
					
					Thread.sleep(SLEEP_100);
				}
				catch (InterruptedException e) 
				{
					System.out.println(e.getClass().getName() + ". Server Listening Again.");
					this.stopWorking();
				} 
				catch (IOException e) 
				{
					System.out.println(e.getClass().getName() + ". Server Listening Again.");
					this.stopWorking();
				} 
				catch (ClassNotFoundException e) 
				{
					System.out.println(e.getClass().getName() + ". Server Listening Again.");
					this.stopWorking();
				}
			 
			}
			
		 
			 
		}
		/**
		 * tell the worker which socket should be working with.
		 * @param socket the socket toward specific client.
		 */
		public void setWorkingPort(Socket socket)
		{
			workingPort = socket;
			
		}
		
		public void stopWorking()
		{
			working = false;
			workingPort = null;
		}
		/**
		 * halt the Worker
		 */
		public void done()
		{
			run = false; 
			
		}
		/**
		 * 
		 * Return whether this thread is free or busy.
		 * @return yes:ready to work. No:running 
		 */
		public boolean ready()
		{
			return !working;
		}
		/**
		 * maintain a private link with client at an special port.
		 * listens for client's command request, process command then return result back.
		 * @param source port number for the current client. 
		 * @pre the socket(source) initiated
		 * @post command processed, and result returned to client.
		 */
		private void individualLink(Socket source)throws IOException, ClassNotFoundException, InterruptedException
		{	
			working = true;
			ObjectOutputStream output = new ObjectOutputStream(source.getOutputStream()); 
			ObjectInputStream input = new ObjectInputStream(source.getInputStream());
						
			Object message = input.readObject();
			output.writeObject(processingRequest(message));
			
			output.flush();
			output.close();
			input.close(); 
			source.close();
			working = false;
					
		}
		
		
		/** 
		 * process the received object
		 * @param command object of Command class (other type are useless)
		 * @pre a valid Command Object (or other parameters)
		 * @post command processed .
		 * @return  return command if it is a string; 
		 * 		 invoke method if it is an object of Command class
		 * 		 return null, if it is object of other classes.
		 */
		public Object processingRequest(Object command) throws InterruptedException
		{
			//print("|Server|..Analysing and processing data");
			//assertTrue(command != null);
			if(command instanceof String)
			{
				//print"|Server|String recieved and is being returned.");
				return command;
			}
			else if(command instanceof Command)
			{
				return this.handleCommand(command);
			}
			else if (command instanceof Phase2.Node)
			{
				return this.handleNode(command);
			}
			//print("|Server|receive an unuseful object" + command.toString());
			return null;
			
			
		}
		
		private Object handleCommand(Object command)
		{
			//print("|Server|Command Recieved");
			
			Object obj = database.get(((Command) command).getObjID());
			
			if (obj == null)
			{
				//print("|Server|Unable to locate Object in database");
				return new NullPointerException("Object referenced in Command not found in database.");
			}
			else
			{
				//print("|Server|Located Object in database");
				try
				{
					return ((Command) command).invoke(obj);
				} 
				catch (InvocationTargetException e)
				{
					//e.printStackTrace();
					return e;
				}
				catch (NoSuchMethodException e)
				{
					//e.printStackTrace();
					return e;
				}
				catch (IllegalAccessException e)
				{
					//e.printStackTrace();
					return e;
				}
				catch (IllegalArgumentException e)
				{
					//e.printStackTrace();
					return e;
				}
			}
		}
		
		private Object handleNode(Object command) throws InterruptedException
		{
			//print("|Server|Node Recieved, inserting node");
			Set<Phase1.GlobalID> keys = this.database.getAllKeys();
			for (Phase1.GlobalID key : keys)
			{
				// if you find a node in the database, insert the command node there
				if (this.database.get(key) instanceof Phase2.Node)
				{
					Phase2.Node node = (Phase2.Node) this.database.get(key);
					//print("|Server| TEST inserting at node:\n" + node + "\n|Server| TEST");
					node.insert((Phase2.Node) command);
					//print("|Server| TEST printing neighbours size: " + node.getNeighbours().size()
							//+ "\n" + node.getNeighbours());
					return null;
				}
			}
			
			// this only runs if there are no nodes in the database
			// it makes a new node, inserts it in the database
			// and then inserts the command node
			Phase2.Node node = new Phase2.Node();
			this.database.put(node.getGlobalID(), node);
			node.insert((Phase2.Node) command);
			
			//print("|Server|Node inserted");
			return null;
		}
	}
}
